在 Redux 中做非同步的請求其實是很麻煩的,這也是一開始讓我卡關的一個痛點,但好在有 StackOverflow 才沒讓這個痛持續很久。
其實一開始我是愛用 redux-thunk 的,但 Redux-Saga 提供了太多方便的 API,讓我可以處理複雜的操作,而且也不會讓程式碼變得難看,這篇會簡單的介紹一些基本的用法,如果有你也對它一見鍾情,那可以到 Redux-Saga 的文檔 查看更詳細的用法。
從 npm 中安裝 Redux-Saga:
npm install --save redux-saga
完成後,還要另外安裝 @babel/polyfill,因為在 Redux-Saga 中會出現 function*
,它的名字叫做 Generator Function,像 Arrow Function 一樣,是 JavaScript 中的一種 function。
例如下方是一般的 function,只要執行了就會直接從 0 輸出到 10:
function printNumber() {
for (let i = 0; i <= 10; i += 1) {
console.log(i);
}
}
printNumber() // 0 1 2 3 ... 10
但是如果用 Generator Function 配合上 yield 使用,就能讓程式的運行停在標記 yield 的地方,等待下一次執行:
function* printNumber() {
for (let i = 0; i <= 10; i += 1) {
yield console.log(i);
}
}
// 將 printNumber 的執行交給
const iteratorA = printNumber();
// 接著每一次執行 .next() 都會到 Generator Function 中的 yield,然後停住
iteratorA.next() // 0
// 下一次執行 .next() 時又會執行到 yield,然後停住
iteratorA.next() // 1
// 依此類推,一直到 10
iteratorA.next() // 10
// 再來就沒有了
iteratorA.next()
但是這麼方便的 function 支援度還不是那麼高,所以才要另外安裝 @babel/polyfill ,它能替我們處理使用到 Generator Function 或其他瀏覽器為支援語法的應對方式。
安裝 @babel/polyfill :
npm install --save @babel/polyfill
接著打開 webpack.config.js,在 entry 中設置編譯時載入 @babel/polyfill :
module.exports = {
entry: ['@babel/polyfill', './src/index.jsx'],
/* 其餘省略 */
};
首先解釋一下使用了 Redux-Saga 後,原本流程的變化:
原 Redux 流程為先對 Component 做 connect,之後便可使用 Store 的 dispatch
觸發預先在 Reducer 中寫好的邏輯。
而 Redux-Saga 的流程同樣是先對 Component 做 connect,但 dispatch
觸發的事件為 React-Saga 預先訂閱的名稱,在該事件裡才依照流程去觸發需執行的 Reducer 邏輯。
換句話說
如果對這個流程沒有問題,那就能繼續下去,如果還有點搞不清楚,那看完下方的操作說明後可以再回顧一次,應該會更清楚!
以下的 API 請求會從 httpbin 這個網站拿現成的來用,大家無聊想玩 API 的時候也可以玩看看。
首先在 src/action/todolist.js 中定義 Action:
export const FETCH_DATA_BEGIN = 'FETCH_DATA_BEGIN';
export const fetchDataBegin = () => ({
type: FETCH_DATA_BEGIN,
});
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
FETCH_DATA_BEGIN
用來觸發請求的事件,FETCH_DATA_SUCCESS
是請求完成後執行的事件。
接下來是執行的 function 內容,我們需要使用 Fetch 獲取資料,然後送到 Reducer 中更新 State,這個階段我們會使用到 Redux-Saga 中的 call
和 put
,這兩個被稱作 Effect,官網的說明如下:
當一個 middleware 透過 Saga 取得一個被 yield 的 Effect,Saga 會被暫停,直到 Effect 被完成。
簡單來說就是在獲得 Effect 執行完的結果前,都不會往下一行執行,而 call
用來執行 function,在此我們將請求的 Fetch 寫在 call
中,讓我們的 fetchData
可以等到拿到資料後再繼續執行:
import { call } from 'redux-saga/effects';
function* fetchData() {
// 使用 data 接收請求的資料
const data = yield call(
() => fetch('https://httpbin.org/get')
.then(response => response.json()),
);
}
用 call
取到資料後,就輪到 put
登場了,put
可以作為 dispatch
觸發 Reducer,使用方式也和 dispatch
一模一樣,以下觸發 Reducer 中的 FETCH_DATA_SUCCESS
,將 call
獲取到的資料送到 Reducer 中儲存:
import { call, put } from 'redux-saga/effects';
function* fetchData() {
// ...使用 call 獲取資料
yield put(fetchDataSuccess(data));
}
接下來,記得上方說過「原本被 dispatch 觸發的 Method 從 Reducer 變成 Redux-Saga。」,因此我們必須在 Redux-Saga 中訂閱 FETCH_DATA_BEGIN
執行 fetchData
獲取資料,並寫到 Reducer 中。
通常我們會把訂閱事件統一到一個 Function 中,管理一種 Redcuer 的 State,在 Function 內可以使用 takeEvery
來訂閱相同類別的所有事件:
import { call, put, takeEvery } from 'redux-saga/effects';
function* mySaga() {
yield takeEvery(FETCH_DATA_BEGIN, fetchData);
}
export default mySaga;
mySaga
這個 Generator Function 便是在訂閱事件, Function 內的每一項 takeEvery
都在訂閱一個事件,觸發的 action.type
是 FETCH_DATA_BEGIN
,執行的事件是 fetchData
。
到此, src/action/todolist.js 的準備就先完成了,為了更好的管理散佈在不同檔案間的 Saga,筆者會傾向於在 src 下建立另一個目錄管理 sagas/index.js:
|-src
|-action
|-reducer
|-sagas
|-index.js
|-store
剛好,這時候是個好時間,我把 position 資料夾移除,畢竟他在接下來的文章中不會再有登場的機會。
打開 src/sagas/index.js,將 src/action/todolist.js 中訂閱事件的 mySaga
給 import,並用另一個 Generator Function 包裝所有 Saga,雖然目前僅有一個但如果有多個的話,只需要再 all
的陣列中添加新 Saga 即可:
import { all } from 'redux-saga/effects';
import toodlistSaga from '../action/todolist';
function* rootSaga() {
yield all([
toodlistSaga(),
]);
}
export default rootSaga;
接下去講 Redux-Saga 前,先到 src/reducer/todolist.js 中加上 FETCH_DATA_SUCCESS
的 Action 動作:
/*其餘未更動的程式碼省略*/
const initState = {
todoList: ['第一件事情', '第二件事情'],
data: {},
};
const todoReducer = (state = initState, action) => {
switch (action.type) {
/*省略 ADD_TODO */
case actions.FETCH_DATA_SUCCESS:
return {
...state,
data: action.payload.data,
};
/*省略 default */
}
};
export default todoReducer;
最後一個階段,要把剛剛設置的 mySaga 創建成一個 Middleware,放進 Store 中,這樣子如果我們用 dispatch
觸發到用 takeEvery
訂閱的事件,那 Redux-Saga 就會替我們處理事情了。
打開 src/store/index.js 從 Redux-Saga 取出 createSagaMiddleware
,還有 src/sagas/index.js 中管理訂閱事件的 rootSaga
:
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';
之後使用 createSagaMiddleware
創建一個 Saga 的 Middleware,並放入 applyMiddleware
中給 createStore
創建 store
:
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
todoReducer,
applyMiddleware(sagaMiddleware, logger),
);
最後一步就是用 sagaMiddleware
的 run
,來執行上方用來訂閱事件的 rootSaga
:
sagaMiddleware.run(rootSaga);
一切準備就緒後,回到 src/index.jsx 中建立一個 Component 為 Content
,在 Component 中放上一個按鈕用 dispatch
觸發 FETCH_DATA_BEGIN
,並將資料顯示在畫面上:
src/index.jsx :
然後把這個 Content
放到 Main
裡面,Render 到畫面上:
運行指令 npm run start
,點擊獲得資料按鈕後,就能看見資料出現了,因為我們有設定 redux-logger
,所以也能看見事件的運行順序:
本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)
看完後應該有人會想說,不就是請求 API,幹嘛搞那麼複雜?
嗯對的,但是 Redux-Saga 擁有的是他的便利和優雅,它會在底層幫你處理 Promise,直接取得你要的資料,不但減少程式的複雜度,在 coding 的時候也不會被一堆 return
的 promise
搞得混亂。
而文中介紹的只是基本的使用方法,除了 call
、 put
和 takeEvery
外還有像是 select
、take
都是很好用的 Effect ,官方的文檔也寫得很完整,Redux-Saga 是在 Redux 中處理非同步資料流套件的一個好選擇。
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
有點複雜的一個章節,還是不太懂什麼時候需要用到redux-saga,弄了好久
JSON.stringify(data)居然是空白資料,但是按獲得資料時確實可以獲得資料,redux-logger也能顯示新增資料後的資料,但是逐一檢視後還是找不到問題點,也沒有警示,QQ。
有需要的話我可以幫忙看程式碼~~
至於用 redux-sage 或是 redux-thunk 等套件的時機是用來處理非同步(打 API 等等)的資料用的!
好的,我再反覆看過,有問題再向神Q請教!
最近剛了解closure、promise,有點不太好上手,特別是非同步的地方XDD。